#include "Perceptron.h"

// Carrega um perceptron salvo no arquivo passado
Perceptron::Perceptron(const char *file)
{
    CSVFile f(file, CSV_IN);

    vdouble vd(2);
    f.read(vd);
    nInVars = vd.getuint(0);
    nOutVars = vd.getuint(1);

    vd.resize(1);
    f.read(vd);
    learningRate = vd.get(0);

    activation = new Activation(STEP);
    learning = new Learning(SUPERVISIONED, learningRate);

    for(uint i = 0; i < nOutVars; i++)
    {
        assert(f.has());

        vd.resize(nInVars + 1);
        f.read(vd);
        Neuron *n = new Neuron(i, nInVars, *activation, *learning, vd);

        neurons.push_back(n);
    }
    assert(!f.has());

    print();
}

// Constrói um perceptron passando o nº de variávies de entrada, de saída e a taxa de aprendizado */
Perceptron::Perceptron(uint nInVars, uint nOutVars, double learningRate)
{
    // Garante que o nº de variáveis de entrada e de saída são maiores que 0
    assert(nInVars > 0);
    assert(nOutVars > 0);

    this->nInVars = nInVars;
    this->nOutVars = nOutVars;
    this->learningRate = learningRate;

    activation = new Activation(STEP);
    learning = new Learning(SUPERVISIONED, learningRate);

    for(uint i = 0; i < nOutVars; i++)
    {
        Neuron *n = new Neuron(i, nInVars, *activation, *learning);
        neurons.push_back(n);
    }

    print();
}

Perceptron::~Perceptron()
{
    delete learning;
    delete activation;
    for(uint i = 0; i < nOutVars; i++)
        delete neurons[i];
}

// Salva o perceptron em um arquivo
void Perceptron::saveToFile(const char *file)
{
    CSVFile f(file, CSV_OUT);

    f.write(nInVars);
    f.write(nOutVars);
    f.wendl();

    f.write(learningRate);
    f.wendl();

    for(uint i = 0; i < neurons.size(); i++)
    {
        f.write(neurons[i]->getWeightVector());
        f.wendl();
    }
}

// Treina o perceptron passando um conjunto de entradas e saídas
void Perceptron::train(DataSet& ds, uint maxIt)
{
    cout << "#=| PERCEPTRON TRAINING |==============================#" << endl << endl;

    // Garante que o número de variáveis de entrada e de saída sejam o mesmo
    assert(nInVars == ds.getNumberOfInVars());
    assert(nOutVars == ds.getNumberOfOutVars());

    struct timespec initT, endT;
    clock_gettime(CLOCK_REALTIME, &initT);

    for(uint i = 0; i < neurons.size(); i++) // Loop para cada neurônio
    {
        cout << "Neuron " << (i + 1) << endl << endl;

        uint cycle = 0;
        uint learn;
        do
        {
            learn = 0;
            cout << " |--> Cycle " << cycle + 1 << endl;
            cycle++;

            for(uint j = 0; j < ds.size(); j++) // Loop para cada entrada/saída
            {
                Data *data = ds.get(j); // Recupera a entrada/saída a ser treinada

                cout << "    |--> Input/Output " << (j + 1) << ":   ";
                data->printInput();
                cout << endl;

                double neuronOutput = neurons[i]->getOutput(data->getInputVector()); // Gera a saida do neurônio i para a entrada j
                double expected = data->getOutputVector().get(i); // Recupera a saída esperada para a entrada j
                double delta = neuronOutput - expected; // Calcula a diferença entre as duas

                cout << "       |--> Neuron output:   " << neuronOutput << endl;
                cout << "       |--> Expected output: " << expected << endl;
                cout << "       |--> Difference:      " << delta << endl;

                data->setNeuralOutput(i, neuronOutput);

                if(fabs(delta) > DELTA) // Verifica se a diferença está entre uma faixa razoável
                {
                    cout << "           ==| ERROR |==";
                    neurons[i]->train(data->getInputVector(), delta); // Treina o neurônio
                    cout << " New weights: ";
                    neurons[i]->printWeights();
                    cout << endl << endl;
                }
                else
                {
                    cout << "           ==| OK |==" << endl << endl;
                    learn++;
                }
            }

        }
        while(learn != ds.size() && cycle < maxIt); // Processo é repetido até que o neurônio acerte todas as entradas
    }

    ds.calculateSuccessRate();

    clock_gettime(CLOCK_REALTIME, &endT);
    double trainTime = ((endT.tv_sec * (double) 1000) + (endT.tv_nsec / (double) 1000000)) - ((initT.tv_sec * (double) 1000) + (initT.tv_nsec / (double) 1000000));
    ds.setTime(trainTime);

    cout << endl << "Success rate: " << ds.getSuccessRate() << endl;
    cout << endl << "Training time: " << trainTime << " msec" << endl << endl;

    cout << "#======================================================#" << endl << endl;
}

void Perceptron::test(DataSet& ds)
{
    cout << "#=| PERCEPTRON TESTING |===============================#" << endl << endl;

    // Garante que o número de variáveis de entrada e de saída sejam o mesmo
    assert(nInVars == ds.getNumberOfInVars());
    assert(nOutVars == ds.getNumberOfOutVars());

    struct timespec initT, endT;
    clock_gettime(CLOCK_REALTIME, &initT);

    for(uint j = 0; j < ds.size(); j++) // Loop para cada entrada
    {
        cout << "Input/Output " << (j + 1) << endl;

        Data *data = ds.get(j);

        cout << " |--> Input        : ";
        data->printInput();
        cout << endl;

        cout << " |--> Output       : ";
        data->printOutput();
        cout << endl;

        cout << " |--> Neural Output: ";

        for(uint i = 0; i < neurons.size(); i++) // Loop para cada neurônio
        {
            double out = neurons[i]->getOutput(data->getInputVector());
            cout << out << " ";
            data->setNeuralOutput(i, out);
        }
        cout << endl;
    }

    ds.calculateSuccessRate();

    clock_gettime(CLOCK_REALTIME, &endT);
    double testTime = ((endT.tv_sec * (double) 1000) + (endT.tv_nsec / (double) 1000000)) - ((initT.tv_sec * (double) 1000) + (initT.tv_nsec / (double) 1000000));
    ds.setTime(testTime);

    cout << endl << "Success rate: " << ds.getSuccessRate() << endl;
    cout << endl << "Test time: " << testTime << " msec" << endl << endl;

    cout << "#======================================================#" << endl << endl;
}

// Reseta os pesos de todos os neurônios
void Perceptron::resetNeuronsWeights()
{
    for(uint i = 0; i < neurons.size(); i++)
        neurons[i]->resetWeights();
}

void Perceptron::print()
{
    cout << "#=| PERCEPTRON |=======================================#" << endl << endl;

    cout << "Number of input variables: " << nInVars << endl;
    cout << "Number of output variables: " << nOutVars << endl;
    cout << "Learning rate: " << learningRate << endl << endl;

    for(uint i = 0; i < nOutVars; i++)
    {
        cout << "Neuron " << (i + 1) << " weights: ";
        neurons[i]->printWeights();
        cout << endl;
    }
    cout << endl << "#======================================================#" << endl << endl;
}

